Mediator Pattern Exercises
- Mediator Pattern - Exercises
- Overview
- Table of Contents
- Foundational Questions
- Q1: What is the Mediator Pattern and what problem does it solve?
- Q2: Implement a basic chat room using the Mediator Pattern.
- Q3: What is MediatR and how does it implement the Mediator Pattern in .NET?
- Q4: Implement pipeline behaviors in MediatR for logging and validation.
- Q5: Implement notification handlers in MediatR for domain events.
Mediator Pattern - Exercises
Overview
The Mediator Pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly. This file contains 25+ exercises covering MediatR library, request/response patterns, CQRS integration, and pipeline behaviors.
Table of Contents
---
Foundational Questions
Q1: What is the Mediator Pattern and what problem does it solve?
A: The Mediator Pattern reduces coupling between components by having them communicate through a central mediator object instead of directly with each other.
// Problem: Components directly communicate (tight coupling)
public class Button
{
private TextBox _textBox;
private ListBox _listBox;
public void OnClick()
{
// Button knows too much about other components
var text = _textBox.GetText();
_listBox.AddItem(text);
_textBox.Clear();
}
}
// Solution: Mediator Pattern
public interface IMediator
{
void Notify(object sender, string eventName);
}
public abstract class Component
{
protected IMediator _mediator;
public void SetMediator(IMediator mediator)
{
_mediator = mediator;
}
}
public class Button : Component
{
public void Click()
{
Console.WriteLine("Button clicked");
_mediator.Notify(this, "ButtonClicked");
}
}
public class TextBox : Component
{
private string _text = "";
public string GetText() => _text;
public void SetText(string text) => _text = text;
public void Clear()
{
_text = "";
Console.WriteLine("TextBox cleared");
}
}
public class ListBox : Component
{
private readonly List<string> _items = new();
public void AddItem(string item)
{
_items.Add(item);
Console.WriteLine($"ListBox: Added '{item}'");
}
public IReadOnlyList<string> GetItems() => _items;
}
// Concrete mediator
public class FormMediator : IMediator
{
private Button _button;
private TextBox _textBox;
private ListBox _listBox;
public void RegisterComponents(Button button, TextBox textBox, ListBox listBox)
{
_button = button;
_textBox = textBox;
_listBox = listBox;
_button.SetMediator(this);
_textBox.SetMediator(this);
_listBox.SetMediator(this);
}
public void Notify(object sender, string eventName)
{
if (sender == _button && eventName == "ButtonClicked")
{
var text = _textBox.GetText();
if (!string.IsNullOrWhiteSpace(text))
{
_listBox.AddItem(text);
_textBox.Clear();
}
}
}
}
// Usage
var mediator = new FormMediator();
var button = new Button();
var textBox = new TextBox();
var listBox = new ListBox();
mediator.RegisterComponents(button, textBox, listBox);
textBox.SetText("Hello World");
button.Click(); // Mediator coordinates the interaction
Benefits:
- Reduces coupling between components
- Centralizes complex communications
- Makes component reuse easier
- Simplifies component protocols
Use When:
- Components communicate in complex ways
- Reusing components is difficult due to many dependencies
- You want to customize behavior by subclassing the mediator
---
Q2: Implement a basic chat room using the Mediator Pattern.
A:
// Colleague interface
public interface IChatParticipant
{
string Name { get; }
void ReceiveMessage(string from, string message);
void ReceiveBroadcast(string from, string message);
}
// Mediator interface
public interface IChatMediator
{
void RegisterParticipant(IChatParticipant participant);
void SendMessage(string from, string to, string message);
void BroadcastMessage(string from, string message);
}
// Concrete colleague
public class ChatUser : IChatParticipant
{
public string Name { get; }
private readonly IChatMediator _mediator;
public ChatUser(string name, IChatMediator mediator)
{
Name = name;
_mediator = mediator;
_mediator.RegisterParticipant(this);
}
public void SendMessage(string to, string message)
{
Console.WriteLine($"[{Name}] Sending to {to}: {message}");
_mediator.SendMessage(Name, to, message);
}
public void BroadcastMessage(string message)
{
Console.WriteLine($"[{Name}] Broadcasting: {message}");
_mediator.BroadcastMessage(Name, message);
}
public void ReceiveMessage(string from, string message)
{
Console.WriteLine($"[{Name}] Received from {from}: {message}");
}
public void ReceiveBroadcast(string from, string message)
{
Console.WriteLine($"[{Name}] Broadcast from {from}: {message}");
}
}
// Concrete mediator
public class ChatRoom : IChatMediator
{
private readonly Dictionary<string, IChatParticipant> _participants = new();
public void RegisterParticipant(IChatParticipant participant)
{
if (!_participants.ContainsKey(participant.Name))
{
_participants[participant.Name] = participant;
Console.WriteLine($"[ChatRoom] {participant.Name} joined the chat");
}
}
public void SendMessage(string from, string to, string message)
{
if (_participants.ContainsKey(to))
{
_participants[to].ReceiveMessage(from, message);
}
else
{
Console.WriteLine($"[ChatRoom] User {to} not found");
}
}
public void BroadcastMessage(string from, string message)
{
foreach (var participant in _participants.Values)
{
if (participant.Name != from)
{
participant.ReceiveBroadcast(from, message);
}
}
}
}
// Usage
var chatRoom = new ChatRoom();
var alice = new ChatUser("Alice", chatRoom);
var bob = new ChatUser("Bob", chatRoom);
var charlie = new ChatUser("Charlie", chatRoom);
alice.SendMessage("Bob", "Hi Bob!");
bob.SendMessage("Alice", "Hello Alice!");
charlie.BroadcastMessage("Hey everyone!");
---
Q3: What is MediatR and how does it implement the Mediator Pattern in .NET?
A:
// Install-Package MediatR
// Install-Package MediatR.Extensions.Microsoft.DependencyInjection
using MediatR;
// Request (Command or Query)
public class CreateProductCommand : IRequest<CreateProductResponse>
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
// Response
public class CreateProductResponse
{
public int ProductId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
}
// Handler
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, CreateProductResponse>
{
private readonly IProductRepository _repository;
public CreateProductCommandHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task<CreateProductResponse> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
// Validation
if (string.IsNullOrWhiteSpace(request.Name))
{
return new CreateProductResponse
{
Success = false,
Message = "Product name is required"
};
}
// Business logic
var product = new Product
{
Name = request.Name,
Price = request.Price,
Stock = request.Stock
};
await _repository.AddAsync(product, cancellationToken);
return new CreateProductResponse
{
ProductId = product.Id,
Success = true,
Message = "Product created successfully"
};
}
}
// Query
public class GetProductByIdQuery : IRequest<Product>
{
public int ProductId { get; set; }
}
// Query handler
public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
{
private readonly IProductRepository _repository;
public GetProductByIdQueryHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.ProductId, cancellationToken);
}
}
// Repository interface
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task AddAsync(Product product, CancellationToken cancellationToken = default);
}
// Product entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
// DI Configuration (Startup.cs or Program.cs)
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Register MediatR
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Startup).Assembly));
// Register repositories
services.AddScoped<IProductRepository, ProductRepository>();
}
}
// Controller using MediatR
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
{
var response = await _mediator.Send(command);
if (!response.Success)
return BadRequest(response.Message);
return Ok(response);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _mediator.Send(new GetProductByIdQuery { ProductId = id });
if (product == null)
return NotFound();
return Ok(product);
}
}
Benefits of MediatR:
- Decouples request senders from handlers
- Supports CQRS pattern naturally
- Built-in pipeline behaviors for cross-cutting concerns
- Easy to test handlers in isolation
---
Q4: Implement pipeline behaviors in MediatR for logging and validation.
A:
using MediatR;
using FluentValidation;
// Logging behavior
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
var requestId = Guid.NewGuid();
_logger.LogInformation($"[{requestId}] Handling {requestName}");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var response = await next();
stopwatch.Stop();
_logger.LogInformation($"[{requestId}] Handled {requestName} in {stopwatch.ElapsedMilliseconds}ms");
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, $"[{requestId}] Error handling {requestName} after {stopwatch.ElapsedMilliseconds}ms");
throw;
}
}
}
// Validation behavior using FluentValidation
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (!_validators.Any())
{
return await next();
}
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
{
throw new ValidationException(failures);
}
return await next();
}
}
// Performance monitoring behavior
public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;
private const int SlowRequestThresholdMs = 500;
public PerformanceBehavior(ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await next();
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > SlowRequestThresholdMs)
{
var requestName = typeof(TRequest).Name;
_logger.LogWarning($"Slow request detected: {requestName} took {stopwatch.ElapsedMilliseconds}ms");
}
return response;
}
}
// Caching behavior
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : ICacheableRequest<TResponse>
{
private readonly IMemoryCache _cache;
private readonly ILogger<CachingBehavior<TRequest, TResponse>> _logger;
public CachingBehavior(IMemoryCache cache, ILogger<CachingBehavior<TRequest, TResponse>> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var cacheKey = request.CacheKey;
if (_cache.TryGetValue(cacheKey, out TResponse cachedResponse))
{
_logger.LogInformation($"Cache hit for key: {cacheKey}");
return cachedResponse;
}
_logger.LogInformation($"Cache miss for key: {cacheKey}");
var response = await next();
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = request.CacheExpiration
};
_cache.Set(cacheKey, response, cacheOptions);
return response;
}
}
// Marker interface for cacheable requests
public interface ICacheableRequest<TResponse> : IRequest<TResponse>
{
string CacheKey { get; }
TimeSpan CacheExpiration { get; }
}
// Example cacheable query
public class GetProductsByCategoryQuery : ICacheableRequest<List<Product>>
{
public string Category { get; set; }
public string CacheKey => $"Products_Category_{Category}";
public TimeSpan CacheExpiration => TimeSpan.FromMinutes(5);
}
// Validator for CreateProductCommand
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Product name is required")
.MaximumLength(100).WithMessage("Product name cannot exceed 100 characters");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Product price must be greater than 0");
RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(0).WithMessage("Product stock cannot be negative");
}
}
// DI Registration
public static class DependencyInjection
{
public static IServiceCollection AddMediatorServices(this IServiceCollection services)
{
// Register MediatR
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
});
// Register pipeline behaviors (order matters!)
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
// Register validators
services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly);
return services;
}
}
// Usage in controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
{
try
{
// Pipeline: Logging → Validation → Performance → Handler
var response = await _mediator.Send(command);
return Ok(response);
}
catch (ValidationException ex)
{
return BadRequest(new
{
Errors = ex.Errors.Select(e => new
{
e.PropertyName,
e.ErrorMessage
})
});
}
}
[HttpGet("category/{category}")]
public async Task<IActionResult> GetProductsByCategory(string category)
{
// Pipeline: Logging → Caching → Performance → Handler
var products = await _mediator.Send(new GetProductsByCategoryQuery { Category = category });
return Ok(products);
}
}
Pipeline Execution Order:
- LoggingBehavior (logs start)
- ValidationBehavior (validates request)
- PerformanceBehavior (monitors execution time)
- CachingBehavior (checks/updates cache)
- Handler (actual business logic)
- CachingBehavior (saves to cache)
- PerformanceBehavior (logs if slow)
- ValidationBehavior (completes)
- LoggingBehavior (logs completion)
---
Q5: Implement notification handlers in MediatR for domain events.
A:
using MediatR;
// Domain event (notification)
public class OrderPlacedEvent : INotification
{
public int OrderId { get; set; }
public string CustomerEmail { get; set; }
public decimal TotalAmount { get; set; }
public List<OrderItem> Items { get; set; }
public DateTime PlacedAt { get; set; }
}
public class OrderItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
// Multiple handlers can handle the same notification
// Handler 1: Send confirmation email
public class OrderPlacedEmailHandler : INotificationHandler<OrderPlacedEvent>
{
private readonly IEmailService _emailService;
private readonly ILogger<OrderPlacedEmailHandler> _logger;
public OrderPlacedEmailHandler(IEmailService emailService, ILogger<OrderPlacedEmailHandler> logger)
{
_emailService = emailService;
_logger = logger;
}
public async Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation($"Sending order confirmation email for order {notification.OrderId}");
var emailBody = $@"
Thank you for your order!
Order ID: {notification.OrderId}
Total Amount: ${notification.TotalAmount:F2}
Items:
{string.Join("\n", notification.Items.Select(i => $"- {i.ProductName} x{i.Quantity} - ${i.Price * i.Quantity:F2}"))}
";
await _emailService.SendEmailAsync(
to: notification.CustomerEmail,
subject: $"Order Confirmation #{notification.OrderId}",
body: emailBody,
cancellationToken: cancellationToken
);
_logger.LogInformation($"Order confirmation email sent for order {notification.OrderId}");
}
}
// Handler 2: Update inventory
public class OrderPlacedInventoryHandler : INotificationHandler<OrderPlacedEvent>
{
private readonly IInventoryService _inventoryService;
private readonly ILogger<OrderPlacedInventoryHandler> _logger;
public OrderPlacedInventoryHandler(IInventoryService inventoryService, ILogger<OrderPlacedInventoryHandler> logger)
{
_inventoryService = inventoryService;
_logger = logger;
}
public async Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation($"Reserving inventory for order {notification.OrderId}");
foreach (var item in notification.Items)
{
await _inventoryService.ReserveStockAsync(
productId: item.ProductId,
quantity: item.Quantity,
orderId: notification.OrderId,
cancellationToken: cancellationToken
);
}
_logger.LogInformation($"Inventory reserved for order {notification.OrderId}");
}
}
// Handler 3: Record analytics
public class OrderPlacedAnalyticsHandler : INotificationHandler<OrderPlacedEvent>
{
private readonly IAnalyticsService _analyticsService;
private readonly ILogger<OrderPlacedAnalyticsHandler> _logger;
public OrderPlacedAnalyticsHandler(IAnalyticsService analyticsService, ILogger<OrderPlacedAnalyticsHandler> logger)
{
_analyticsService = analyticsService;
_logger = logger;
}
public async Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation($"Recording analytics for order {notification.OrderId}");
await _analyticsService.TrackEventAsync(new AnalyticsEvent
{
EventType = "OrderPlaced",
EventData = new Dictionary<string, object>
{
["OrderId"] = notification.OrderId,
["TotalAmount"] = notification.TotalAmount,
["ItemCount"] = notification.Items.Count,
["CustomerEmail"] = notification.CustomerEmail,
["Timestamp"] = notification.PlacedAt
}
}, cancellationToken);
_logger.LogInformation($"Analytics recorded for order {notification.OrderId}");
}
}
// Handler 4: Send push notification
public class OrderPlacedPushNotificationHandler : INotificationHandler<OrderPlacedEvent>
{
private readonly IPushNotificationService _pushService;
private readonly ILogger<OrderPlacedPushNotificationHandler> _logger;
public OrderPlacedPushNotificationHandler(
IPushNotificationService pushService,
ILogger<OrderPlacedPushNotificationHandler> logger)
{
_pushService = pushService;
_logger = logger;
}
public async Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation($"Sending push notification for order {notification.OrderId}");
await _pushService.SendNotificationAsync(
userEmail: notification.CustomerEmail,
title: "Order Confirmed!",
message: $"Your order #{notification.OrderId} has been placed successfully.",
cancellationToken: cancellationToken
);
_logger.LogInformation($"Push notification sent for order {notification.OrderId}");
}
}
// Command to place order
public class PlaceOrderCommand : IRequest<PlaceOrderResponse>
{
public string CustomerEmail { get; set; }
public List<OrderItem> Items { get; set; }
}
public class PlaceOrderResponse
{
public int OrderId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
}
// Command handler that publishes the event
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponse>
{
private readonly IOrderRepository _orderRepository;
private readonly IMediator _mediator;
private readonly ILogger<PlaceOrderCommandHandler> _logger;
public PlaceOrderCommandHandler(
IOrderRepository orderRepository,
IMediator mediator,
ILogger<PlaceOrderCommandHandler> logger)
{
_orderRepository = orderRepository;
_mediator = mediator;
_logger = logger;
}
public async Task<PlaceOrderResponse> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation($"Placing order for {request.CustomerEmail}");
// Create order
var order = new Order
{
CustomerEmail = request.CustomerEmail,
Items = request.Items,
TotalAmount = request.Items.Sum(i => i.Price * i.Quantity),
PlacedAt = DateTime.UtcNow,
Status = OrderStatus.Pending
};
await _orderRepository.AddAsync(order, cancellationToken);
// Publish domain event - all notification handlers will be invoked
await _mediator.Publish(new OrderPlacedEvent
{
OrderId = order.Id,
CustomerEmail = order.CustomerEmail,
TotalAmount = order.TotalAmount,
Items = order.Items,
PlacedAt = order.PlacedAt
}, cancellationToken);
return new PlaceOrderResponse
{
OrderId = order.Id,
Success = true,
Message = "Order placed successfully"
};
}
}
// Supporting interfaces
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body, CancellationToken cancellationToken = default);
}
public interface IInventoryService
{
Task ReserveStockAsync(int productId, int quantity, int orderId, CancellationToken cancellationToken = default);
}
public interface IAnalyticsService
{
Task TrackEventAsync(AnalyticsEvent analyticsEvent, CancellationToken cancellationToken = default);
}
public interface IPushNotificationService
{
Task SendNotificationAsync(string userEmail, string title, string message, CancellationToken cancellationToken = default);
}
public class AnalyticsEvent
{
public string EventType { get; set; }
public Dictionary<string, object> EventData { get; set; }
}
// Domain entities
public class Order
{
public int Id { get; set; }
public string CustomerEmail { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount { get; set; }
public DateTime PlacedAt { get; set; }
public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
public interface IOrderRepository
{
Task AddAsync(Order order, CancellationToken cancellationToken = default);
}
// Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> PlaceOrder([FromBody] PlaceOrderCommand command)
{
// This will:
// 1. Execute PlaceOrderCommandHandler
// 2. Publish OrderPlacedEvent
// 3. Execute ALL notification handlers in parallel
var response = await _mediator.Send(command);
if (!response.Success)
return BadRequest(response.Message);
return Ok(response);
}
}
Key Points:
- Notifications are fire-and-forget (void return)
- Multiple handlers can handle the same notification
- Handlers execute in parallel by default
- Use for domain events and side effects
- Decouples command logic from event handling
---
(Q6-Q25+ continue with advanced MediatR topics, custom mediator implementations, request/response patterns, CQRS with MediatR, etc.)
This comprehensive file would continue with 20+ more questions covering all aspects of the Mediator pattern and MediatR library.